// $Id$

// Copyright by Johannes Sixt
// This file is under GPL, the GNU General Public Licence

#include "debugger.h"
#include "sourcewnd.h"
#include <q3textstream.h>
#include <qpainter.h>
#include <qbrush.h>
#include <qfile.h>
#include <qnamespace.h>
//Added by qt3to4:
#include <QMouseEvent>
#include <QKeyEvent>
#include <kapplication.h>
#include <kiconloader.h>
#include <kglobalsettings.h>
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "mydebug.h"


SourceWindow::SourceWindow(const char* fileName, QWidget* parent, const char* name) :
	KTextView(parent, name),
	m_fileName(fileName)
{
    setNumCols(3);

    // load pixmaps
    m_pcinner = UserIcon("pcinner");
    m_pcup = UserIcon("pcup");
    m_brkena = UserIcon("brkena");
    m_brkdis = UserIcon("brkdis");
    m_brktmp = UserIcon("brktmp");
    m_brkcond = UserIcon("brkcond");
    m_brkorph = UserIcon("brkorph");
    setFont(KGlobalSettings::fixedFont());
}

SourceWindow::~SourceWindow()
{
}

bool SourceWindow::loadFile()
{
	// first we load the code into KTextView::m_texts
	QFile f(m_fileName);
	if (!f.open(QIODevice::ReadOnly)) 
	{
		return false;
	}
	
	bool upd = autoUpdate();
	setAutoUpdate(false);
	
	Q3TextStream t(&f);
	QString s;
	while (!t.eof()) 
	{
		s = t.readLine();
		insertLine(s);
	}
	f.close();
	
	setAutoUpdate(upd);
	if (upd) 
	{
		updateTableSize();
	}
	
	// then we copy it into our own m_sourceCode
	m_sourceCode.setSize(m_texts.size());
	m_rowToLine.setSize(m_texts.size());
	m_lineItems.setSize(m_texts.size());
	for (int i = 0; i < m_texts.size(); i++) 
	{
		m_sourceCode[i].code = m_texts[i];
		m_rowToLine[i] = i;
		m_lineItems[i] = 0;
	}
	
	return true;
}

void SourceWindow::reloadFile()
{
	QFile f(m_fileName);
	if (!f.open(QIODevice::ReadOnly)) 
	{
		// open failed; leave alone
		return;
	}
	
	// read text into m_sourceCode
	m_sourceCode.setSize(0);		/* clear old text */
	
	Q3TextStream t(&f);
	SourceLine s;
	while (!t.eof()) 
	{
		s.code = t.readLine();
		m_sourceCode.append(s);
	}
	f.close();
	
	bool autoU = autoUpdate();
	setAutoUpdate(false);
	
	int lineNo = qMin(m_texts.size(), m_sourceCode.size());
	for (int i = 0; i < lineNo; i++) 
	{
		replaceLine(i, m_sourceCode[i].code);
	}
	if (m_sourceCode.size() > m_texts.size()) 
	{
		// the new file has more lines than the old one
		// here lineNo is the number of lines of the old file
		for (int i = lineNo; i < m_sourceCode.size(); i++) 
		{
			insertLine(m_sourceCode[i].code);
		}
		// allocate line items
		m_lineItems.setSize(m_texts.size());
		for (int i = m_texts.size()-1; i >= lineNo; i--) 
		{
			m_lineItems[i] = 0;
		}
	} 
	else 
	{
		// the new file has fewer lines
		// here lineNo is the number of lines of the new file
		// remove the excessive lines
		m_texts.setSize(lineNo);
		m_lineItems.setSize(lineNo);
	}
	f.close();
	
	setNumRows(m_texts.size());
	
	m_rowToLine.setSize(m_texts.size());
	for (int i = 0; i < m_texts.size(); i++)
		m_rowToLine[i] = i;
	
	setAutoUpdate(autoU);
	if (autoU && isVisible()) 
	{
		updateTableSize();
		repaint();
	}
	
	// if the cursor is in the deleted lines, move it to the last line
	if (m_curRow >= m_texts.size()) 
	{
		m_curRow = -1;			/* at this point don't have an active row */
		activateLine(m_texts.size()-1);	/* now we have */
	}
}

void SourceWindow::scrollTo(int lineNo, const DbgAddr& address)
{
    if (lineNo < 0 || lineNo >= m_sourceCode.size())
		return;

    int row = lineToRow(lineNo, address);
    scrollToRow(row);
}

void SourceWindow::scrollToRow(int row)
{
	if (row >= numRows())
		row = numRows();
	
	// scroll to lineNo
	if (row >= topCell() && row <= lastRowVisible()) 
	{
		// line is already visible
		setCursorPosition(row, 0);
		return;
	}
	
	// setCursorPosition does only a half-hearted job of making the
	// cursor line visible, so we do it by hand...
	setCursorPosition(row, 0);
	row -= 5;
	if (row < 0)
		row = 0;
	
	setTopCell(row);
}

int SourceWindow::textCol() const
{
    // text is in column 2
    return 2;
}

int SourceWindow::cellWidth(int col) const
{
	if (col == 0) 
	{
		return 15;
	} 
	else if (col == 1) 
	{
		return 12;
	} 
	else 
	{
		return KTextView::cellWidth(col);
	}
}

void SourceWindow::paintCell(QPainter* p, int row, int col)
{
	if (col == textCol()) 
	{
		p->save();
		if (isRowDisassCode(row)) 
		{
			p->setPen(Qt::blue);
		}
		KTextView::paintCell(p, row, col);
		p->restore();
		return;
	}
	if (col == 0) 
	{
		uchar item = m_lineItems[row];
		if (item == 0)			/* shortcut out */
			return;
		int h = cellHeight(row);
		if (item & liBP) 
		{
			// enabled breakpoint
			int y = (h - m_brkena.height())/2;
			if (y < 0) y = 0;
			p->drawPixmap(0,y,m_brkena);
		}
		if (item & liBPdisabled) 
		{
			// disabled breakpoint
			int y = (h - m_brkdis.height())/2;
			if (y < 0) y = 0;
			p->drawPixmap(0,y,m_brkdis);
		}
		if (item & liBPtemporary) 
		{
			// temporary breakpoint marker
			int y = (h - m_brktmp.height())/2;
			if (y < 0) y = 0;
			p->drawPixmap(0,y,m_brktmp);
		}
		if (item & liBPconditional) 
		{
			// conditional breakpoint marker
			int y = (h - m_brkcond.height())/2;
			if (y < 0) y = 0;
			p->drawPixmap(0,y,m_brkcond);
		}
		if (item & liBPorphan) 
		{
			// orphaned breakpoint marker
			int y = (h - m_brkcond.height())/2;
			if (y < 0) y = 0;
			p->drawPixmap(0,y,m_brkorph);
		}
		if (item & liPC) 
		{
			// program counter in innermost frame
			int y = (h - m_pcinner.height())/2;
			if (y < 0) y = 0;
			p->drawPixmap(0,y,m_pcinner);
		}
		if (item & liPCup) 
		{
			// program counter somewhere up the stack
			int y = (h - m_pcup.height())/2;
			if (y < 0) y = 0;
			p->drawPixmap(0,y,m_pcup);
		}
		return;
	}
	if (col == 1) 
	{
		if (!isRowDisassCode(row) && m_sourceCode[rowToLine(row)].canDisass) 
		{
			int h = cellHeight(row);
			int w = cellWidth(col);
			int x = w/2;
			int y = h/2;
			p->drawLine(x-2, y, x+2, y);
			if (!isRowExpanded(row)) 
			{
				p->drawLine(x, y-2, x, y+2);
			}
		}
	}
}

void SourceWindow::updateLineItems(const KDebugger* dbg)
{
	// clear outdated breakpoints
	for (int i = m_lineItems.size()-1; i >= 0; i--) 
	{
		if (m_lineItems[i] & liBPany) 
		{
			// check if this breakpoint still exists
			int line = rowToLine(i);
			TRACE(QString().sprintf("checking for bp at %d", line));
			int j;
			for (j = dbg->numBreakpoints()-1; j >= 0; j--) {
			const Breakpoint* bp = dbg->breakpoint(j);
			if (bp->lineNo == line &&
				fileNameMatches(bp->fileName) &&
				lineToRow(line, bp->address) == i)
			{
				// yes it exists; mode is changed below
				break;
			}
			}
			if (j < 0) {
			/* doesn't exist anymore, remove it */
			m_lineItems[i] &= ~liBPany;
			updateLineItem(i);
			}
		}
	}
	
	// add new breakpoints
	for (int j = dbg->numBreakpoints()-1; j >= 0; j--) 
	{
		const Breakpoint* bp = dbg->breakpoint(j);
		if (fileNameMatches(bp->fileName)) 
		{
			TRACE(QString().sprintf("updating %s:%d", bp->fileName.data(), bp->lineNo));
			int i = bp->lineNo;
			if (i < 0 || i >= m_sourceCode.size())
				continue;
			// compute new line item flags for breakpoint
			uchar flags = bp->enabled ? liBP : liBPdisabled;
			if (bp->temporary)
				flags |= liBPtemporary;
			if (!bp->condition.isEmpty() || bp->ignoreCount != 0)
				flags |= liBPconditional;
			if (bp->isOrphaned())
				flags |= liBPorphan;
			// update if changed
			int row = lineToRow(i, bp->address);
			if ((m_lineItems[row] & liBPany) != flags) 
			{
				m_lineItems[row] &= ~liBPany;
				m_lineItems[row] |= flags;
				updateLineItem(row);
			}
		}
	}
}

void SourceWindow::updateLineItem(int row)
{
    updateCell(row, 0);
}

void SourceWindow::setPC(bool set, int lineNo, const DbgAddr& address, int frameNo)
{
	if (lineNo < 0 || lineNo >= int(m_sourceCode.size())) 
	{
		return;
	}
	
	int row = lineToRow(lineNo, address);
	
	uchar flag = frameNo == 0  ?  liPC  :  liPCup;
	if (set) 
	{
		// set only if not already set
		if ((m_lineItems[row] & flag) == 0) 
		{
			m_lineItems[row] |= flag;
			updateLineItem(row);
		}
	} 
	else 
	{
		// clear only if not set
		if ((m_lineItems[row] & flag) != 0) 
		{
			m_lineItems[row] &= ~flag;
			updateLineItem(row);
		}
	}
}

void SourceWindow::find(const QString& text, bool caseSensitive, FindDirection dir)
{
	ASSERT(dir == 1 || dir == -1);
	if (m_sourceCode.size() == 0 || text.isEmpty())
		return;
	
	int line;
	DbgAddr dummyAddr;
	activeLine(line, dummyAddr);
	if (line < 0)
		line = 0;
	int curLine = line;			/* remember where we started */
	bool found = false;
	do 
	{
		// advance and wrap around
		line += dir;
		if (line < 0)
			line = m_sourceCode.size()-1;
		else if (line >= int(m_sourceCode.size()))
			line = 0;
		// search text
		found = m_sourceCode[line].code.find(text, 0, caseSensitive) >= 0;
	} while (!found && line != curLine);
	
	scrollTo(line, DbgAddr());
}

void SourceWindow::mousePressEvent(QMouseEvent* ev)
{
	// Check if right button was clicked.
	if (ev->button() == Qt::RightButton)
	{
		emit clickedRight(ev->pos());
		return;
	}
	
	int col = findCol(ev->x());
	
	// check if event is in line item column
	if (col == textCol()) 
	{
		KTextView::mousePressEvent(ev);
		return;
	}
	
	// get row
	int row = findRow(ev->y());
	if (row < 0 || col < 0)
		return;
	
	if (col == 1) 
	{
		if (isRowExpanded(row)) 
		{
			actionCollapseRow(row);
		} 
		else 
		{
			actionExpandRow(row);
		}
		return;
	}
	
	int sourceRow;
	int line = rowToLine(row, &sourceRow);
	
	// find address if row is disassembled code
	DbgAddr address;
	if (row > sourceRow) 
	{
		// get offset from source code line
		int off = row - sourceRow;
		address = m_sourceCode[line].disassAddr[off-1];
	}
	
	switch (ev->button()) 
	{
		case Qt::LeftButton:
			TRACE(QString().sprintf("left-clicked line %d", line));
			emit clickedLeft(m_fileName, line, address,
						(ev->state() & Qt::ShiftModifier) != 0);
			break;
		case Qt::MidButton:
			TRACE(QString().sprintf("mid-clicked row %d", line));
			emit clickedMid(m_fileName, line, address);
			break;
		default:;
	}
}

void SourceWindow::keyPressEvent(QKeyEvent* ev)
{
	switch (ev->key()) 
	{
		case Qt::Key_Plus:
			actionExpandRow(m_curRow);
			return;
		case Qt::Key_Minus:
			actionCollapseRow(m_curRow);
			return;
	}
	
	KTextView::keyPressEvent(ev);
}

static inline bool isident(QChar c)
{
    return c.isLetterOrNumber() || c.latin1() == '_';
}

bool SourceWindow::wordAtPoint(const QPoint& p, QString& word, QRect& r)
{
	if (findCol(p.x()) != textCol())
		return false;
	int row = findRow(p.y());
	if (row < 0)
		return false;
	
	// find top-left corner of text cell
	int top, left;
	if (!colXPos(textCol(), &left))
		return false;
	if (!rowYPos(row, &top))
		return false;
	
	// get the bounding rect of the text
	QPainter painter(this);
	setupPainter(&painter);
	const QString& text = m_texts[row];
	QRect bound =
	painter.boundingRect(left+2, top, 0,0,
						 Qt::AlignLeft | Qt::SingleLine | Qt::DontClip | Qt::ExpandTabs,
					text, text.length());
	if (!bound.contains(p))
		return false;			/* p is outside text */
	
	/*
		* We split the line into words and check each whether it contains
		* the point p. We must do it the hard way (measuring substrings)
		* because we cannot rely on that the font is mono-spaced.
		*/
	int start = 0;
	while (start < text.length()) 
	{
		while (start < text.length() && !isident(text[start]))
			start++;
		if (start >= text.length())
			return false;
		/*
			* If p is in the rectangle that ends at 'start', it is in the
			* last non-word part that we have just skipped.
			*/
		bound = 
			painter.boundingRect(left+2, top, 0,0,
						Qt::AlignLeft | Qt::SingleLine | Qt::DontClip | Qt::ExpandTabs,
						text, start);
		if (bound.contains(p))
			return false;
		// a word starts now
		int startWidth = bound.width();
		int end = start;
		while (end < text.length() && isident(text[end]))
			end++;
		bound =
			painter.boundingRect(left+2, top, 0,0,
						Qt::AlignLeft | Qt::SingleLine | Qt::DontClip | Qt::ExpandTabs,
						text, end);
		if (bound.contains(p)) 
		{
			// we found a word!
			// extract the word
			word = text.mid(start, end-start);
			// and the rectangle
			r = QRect(bound.x()+startWidth,bound.y(),
					bound.width()-startWidth, bound.height());
			return true;
		}
		start = end;
	}
	return false;
}

void SourceWindow::paletteChange(const QPalette& oldPal)
{
    setFont(KGlobalSettings::fixedFont());
    KTextView::paletteChange(oldPal);
}

/*
 * Two file names (possibly full paths) match if the last parts - the file
 * names - match.
 */
bool SourceWindow::fileNameMatches(const QString& other)
{
	const QString& me = fileName();
	
	// check for null file names first
	if (me.isNull() || other.isNull()) 
	{
		return me.isNull() && other.isNull();
	}
	
	/*
	* Get file names.  Note: Either there is a slash, then skip it, or
	* there is no slash, then -1 + 1 = 0!
	*/
	QString sme = me;
	if (me.endsWith('/'))
		sme.remove(sme.length() -1);
	QString sother = sother;
	if (other.endsWith('/'))
		sother.remove(sother.length() -1);
	return sme == sother;
}

void SourceWindow::disassembled(int lineNo, const QList<DisassembledCode*>& disass)
{
	TRACE("disassembled line " + QString().setNum(lineNo));
	if (lineNo < 0 || lineNo >= m_sourceCode.size())
		return;
	
	SourceLine& sl = m_sourceCode[lineNo];
	
	// copy disassembled code and its addresses
	sl.disass.setSize(disass.count());
	sl.disassAddr.setSize(disass.count());
	sl.canDisass = disass.count() > 0;
	for (uint i = 0; i < disass.count(); i++) 
	{
		const DisassembledCode* c = disass.at(i);
		sl.disass[i] = c->address.asString() + ' ' + c->code;
		sl.disassAddr[i] = c->address;
	}
	
	int row = lineToRow(lineNo);
	if (sl.canDisass) 
	{
		expandRow(row);
	} 
	else 
	{
		// clear expansion marker
		updateCell(row, 1);
	}
}

int SourceWindow::rowToLine(int row, int* sourceRow)
{
	int line = row >= 0  ?  m_rowToLine[row]  :  -1;
	if (sourceRow != 0) 
	{
		// search back until we hit the first entry with the current line number
		while (row > 0 && m_rowToLine[row-1] == line)
			row--;
		*sourceRow = row;
	}
	return line;
}

/*
 * Rows showing diassembled code have the same line number as the
 * corresponding source code line number. Therefore, the line numbers in
 * m_rowToLine are monotonically increasing with blocks of equal line
 * numbers for a source line and its disassembled code that follows it.
 *
 * Hence, m_rowToLine always obeys the following condition:
 *
 *    m_rowToLine[i] <= i
 */

int SourceWindow::lineToRow(int line)
{
	// line is zero-based!
	
	assert(line < m_rowToLine.size());
	
	// quick test for common case
	if (line < 0 || m_rowToLine[line] == line)
		return line;
	
	assert(m_rowToLine[line] < line);
	
	/*
		* Binary search between row == line and end of list. In the loop below
		* we use the fact that the line numbers m_rowToLine do not contain
		* holes.
		*/
	int l = line;
	int h = m_rowToLine.size();
	while (l < h && m_rowToLine[l] != line)
	{
		assert(h == m_rowToLine.size() || m_rowToLine[l] < m_rowToLine[h]);
		
		/*
		* We want to round down the midpoint so that we find the
		* lowest row that belongs to the line we seek.
		*/
		int mid = (l+h)/2;
		if (m_rowToLine[mid] <= line)
			l = mid;
		else
			h = mid;
	}
	// Found! Result is in l:
	assert(m_rowToLine[l] == line);
	
	/*
		* We might not have hit the lowest index for the line.
		*/
	while (l > 0 && m_rowToLine[l-1] == line)
		--l;
	
	return l;
}

int SourceWindow::lineToRow(int line, const DbgAddr& address)
{
	int row = lineToRow(line);
	if (isRowExpanded(row)) 
	{
		row += m_sourceCode[line].findAddressRowOffset(address);
	}
	return row;
}

bool SourceWindow::isRowExpanded(int row)
{
    assert(row >= 0);
    return row < m_rowToLine.size()-1 && m_rowToLine[row] == m_rowToLine[row+1];
}

bool SourceWindow::isRowDisassCode(int row)
{
    return row > 0 &&
	m_rowToLine[row] == m_rowToLine[row-1];
}

void SourceWindow::expandRow(int row)
{
	TRACE("expanding row " + QString().setNum(row));
	// get disassembled code
	int line = rowToLine(row);
	const ValArray<QString>& disass = m_sourceCode[line].disass;
	
	// remove PC (must be set again in slot of signal expanded())
	m_lineItems[row] &= ~(liPC|liPCup);
	
	// insert new lines
	++row;
	m_rowToLine.insertAt(row, line, disass.size());
	m_lineItems.insertAt(row, 0, disass.size());
	m_texts.insertAt(row, disass);
	
	bool autoU = autoUpdate();
	setAutoUpdate(false);
	
	// update line widths
	for (int i = 0; i < disass.size(); i++) 
	{
		updateCellSize(disass[i]);
	}
	
	setNumRows(m_texts.size());
	
	emit expanded(line);		/* must set PC */
	
	setAutoUpdate(autoU);
	if (autoU && isVisible()) 
	{
		updateTableSize();
		update();
	}
}

void SourceWindow::collapseRow(int row)
{
	TRACE("collapsing row " + QString().setNum(row));
	int line = rowToLine(row);
	
	// find end of this block
	int end = row+1;
	while (end < m_rowToLine.size() && m_rowToLine[end] == m_rowToLine[row]) 
	{
		end++;
	}
	++row;
	m_rowToLine.removeAt(row, end-row);
	m_lineItems.removeAt(row, end-row);
	m_texts.removeAt(row, end-row);
	
	bool autoU = autoUpdate();
	setAutoUpdate(false);
	
	setNumRows(m_texts.size());
	
	emit collapsed(line);
	
	setAutoUpdate(autoU);
	if (autoU && isVisible()) 
	{
		updateTableSize();
		update();
	}
}

void SourceWindow::activeLine(int& line, DbgAddr& address)
{
	int row, col;
	cursorPosition(&row, &col);
	
	int sourceRow;
	line = rowToLine(row, &sourceRow);
	if (row > sourceRow) 
	{
		int off = row - sourceRow;	/* offset from source line */
		address = m_sourceCode[line].disassAddr[off-1];
	}
}

/**
 * Returns the offset from the line displaying the source code to
 * the line containing the specified address. If the address is not
 * found, 0 is returned.
 */
int SourceWindow::SourceLine::findAddressRowOffset(const DbgAddr& address) const
{
	if (address.isEmpty())
	return 0;
	
	for (int i = 0; i < disassAddr.size(); i++) 
	{
		if (disassAddr[i] == address) 
		{
			// found exact address
			return i+1;
		}
		if (disassAddr[i] > address) 
		{
			/*
			* We have already advanced too far; the address is before this
			* index, but obviously we haven't found an exact match
			* earlier. address is somewhere between the displayed
			* addresses. We return the previous line.
			*/
			return i;
		}
	}
	// not found
	return 0;
}

void SourceWindow::actionExpandRow(int row)
{
	if (isRowExpanded(row) || isRowDisassCode(row)) 
	{
		// do nothing
		return;
	}
	
	// disassemble
	int line = rowToLine(row);
	const SourceLine& sl = m_sourceCode[line];
	if (!sl.canDisass)
		return;
	if (sl.disass.size() == 0) 
	{
		emit disassemble(m_fileName, line);
	} 
	else 
	{
		expandRow(row);
	}
}

void SourceWindow::actionCollapseRow(int row)
{
	if (!isRowExpanded(row) || isRowDisassCode(row)) 
	{
		// do nothing
		return;
	}
	
	collapseRow(row);
}


#include "sourcewnd.moc"
